package dryven.persistence.provider;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.sql.DataSource;

import dryven.annotations.Connection;
import dryven.config.DatabaseConfiguration;
import dryven.config.DatabaseConfigurationSource;
import dryven.discovery.ClassDiscoveryListener;
import dryven.discovery.DiscoveryConsumer;
import dryven.discovery.annotation.Annotation;
import dryven.discovery.annotation.AnnotationParam;
import dryven.reflection.TypeRef;

public abstract class JpaPersistenceProvider implements DiscoveryConsumer,
		PersistenceProvider {

	private DatabaseConfigurationSource dbCfgSource;
	private Map<DatabaseConfiguration, List<String>> annotatedClasses = new HashMap<DatabaseConfiguration, List<String>>();
	private Map<DatabaseConfiguration, EntityManagerFactory> factories = new HashMap<DatabaseConfiguration, EntityManagerFactory>();
	
	public JpaPersistenceProvider(DatabaseConfigurationSource dbCfgSource) {
		super();
		this.dbCfgSource = dbCfgSource;
	}

	@Override
	public EntityManager createEntityManager(DatabaseConfiguration config) {
		EntityManagerFactory factory = factories.get(config);
		if(factory==null) {
			factory = createFactory(config, annotatedClasses.get(config));
			factories.put(config, factory);
		}
		return factory.createEntityManager();
	}
	
	protected EntityManagerFactory createFactory(DatabaseConfiguration cfg, List<String> annotatedClasses) {
		TempClassLoader loader = new TempClassLoader(getClass().getClassLoader());
		try {
			PersistenceUnitInfo puInfo = createPersistenceUnitInfo(cfg, annotatedClasses, loader);
			javax.persistence.spi.PersistenceProvider provider = getPersistenceProviderClass().newInstance();
			EntityManagerFactory factory = provider.createContainerEntityManagerFactory(puInfo, getProviderProperties());
			return factory;
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			loader.dispose();
		}
	}
	
	protected Map getProviderProperties() {
		return null;
	}
	
	protected PersistenceUnitInfo createPersistenceUnitInfo(DatabaseConfiguration cfg, List<String> annotatedClasses, ClassLoader tempLoader) {
		DataSource source = createDataSource(cfg);
		return new DryvenPersistenceUnitInfo(
				annotatedClasses, 
				getPersistenceProviderClass().getName(), 
				source, 
				cfg.getConnectionName(), 
				tempLoader, 
				getClass().getClassLoader()
			);
	}

	protected abstract Class<? extends javax.persistence.spi.PersistenceProvider> getPersistenceProviderClass();
	
	protected abstract DataSource createDataSource(DatabaseConfiguration cfg);
	
	@Override
	public ClassDiscoveryListener getDiscoveryListener() {
		return new AnnotationReader(dbCfgSource, annotatedClasses);
	}

	@Override
	public void cleanup(DatabaseConfiguration config) {
		for(EntityManagerFactory factory : factories.values()) {
			try {
				factory.close();
			} catch(Exception e) {}
		}
		factories.clear();
	}

}

class AnnotationReader implements ClassDiscoveryListener {

	private String currentClassName;
	private String currentConnectionName;
	private boolean currentClassIsEntity = false;
	private DatabaseConfigurationSource dbSourceCfg;
	private Map<DatabaseConfiguration, List<String>> annotatedClasses;

	public AnnotationReader(DatabaseConfigurationSource dbSourceCfg,
			Map<DatabaseConfiguration, List<String>> annotatedClasses) {
		super();
		this.dbSourceCfg = dbSourceCfg;
		this.annotatedClasses = annotatedClasses;
	}

	@Override
	public void onClass(String typeName, String superClassName,
			String[] interfaces) {
		currentClassName = typeName;
		currentClassIsEntity = false;
		if (dbSourceCfg.getDefaultDatabaseConfiguration() != null) {
			currentConnectionName = dbSourceCfg
					.getDefaultDatabaseConfiguration().getConnectionName();
		}
	}

	@Override
	public void onClassAnnotation(Annotation a) {
		if (a.getType().equals(javax.persistence.Entity.class.getName())) {
			currentClassIsEntity = true;
		} else if (a.getType().equals(Connection.class.getName())) {
			for (AnnotationParam p : a.getParams()) {
				if (p.getName().equals("value")) {
					currentConnectionName = (String) p.getValue();
				}
			}
		}
	}

	@Override
	public void onClassEnd(String typeName) {
		if (currentClassIsEntity) {
			DatabaseConfiguration dbCfg = dbSourceCfg
					.getDatabaseConfigurationByName(currentConnectionName);
			if (dbCfg == null) {
				throw new RuntimeException(
						String.format(
								"Connection name specified for class %s in @%s could not be found in the application configuration",
								currentClassName,
								Connection.class.getName()));
			}
			List<String> annotatedClassesForConnection = annotatedClasses.get(dbCfg);
			if (annotatedClassesForConnection == null) {
				annotatedClassesForConnection = new ArrayList<String>();
				annotatedClasses.put(dbCfg, annotatedClassesForConnection);
			}
			annotatedClassesForConnection.add(currentClassName);
		}
	}

	@Override
	public boolean onConstructor(int access, String typeName, TypeRef[] types) {
		return false;
	}

	@Override
	public void onConstructorAnnotation(Annotation a) {}

	@Override
	public boolean onField(int access, TypeRef type, String name) {
		return false;
	}

	@Override
	public void onFieldAnnotation(Annotation a) {}

	@Override
	public boolean onMethod(int access, String typeName, String methodName,
			TypeRef[] types, TypeRef returnType) {
		return false;
	}

	@Override
	public void onMethodAnnotation(Annotation a) {}

}